# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import time
import json
import numpy as np
from datetime import datetime, timedelta
from utils.commons import retry_wrapper, truncate
from utils.dingding import send_msg_for_order, send_wechat_work_msg
from api.market import fetch_binance_ticker_data
from config import order_spot_money_limit, order_swap_money_limit


# 下单
def place_order(exchange, symbol_order, symbol_last_price, min_qty, price_precision, min_notional, symbol_type='swap',
                wechat_webhook_url='', slip_rate=0.015):
    """
    根据计算好的下单数据，进行下单

    :param exchange:            交易所对象
    :param symbol_order:        计算好的币种下单数据
    :param symbol_last_price:   所有币种最新ticker价格
    :param min_qty:             币种最小下单精度
    :param price_precision:     币种最小价格精度
    :param min_notional:        币种最小下单金额
    :param symbol_type:         下单类型。swap/spot
    :param wechat_webhook_url:  机器人人地址
    :param slip_rate:           滑点。默认0.015下单
    """
    # 小细节：现货下单需要先卖出，再买入(下单金额从小到大排序即可)
    symbol_order.sort_values('实际下单资金', ascending=True, inplace=True)
    symbol_order.set_index('symbol', inplace=True)  # 重新设置index

    # 遍历symbol_order，构建每个币种的下单参数
    order_param_list = []
    for symbol, row in symbol_order.iterrows():
        # ===若当前币种没有最小下单精度、或最小价格精度，报错
        if (symbol not in min_qty) or (symbol not in price_precision):
            # 报错
            raise Exception('当前币种没有最小下单精度、或最小价格精度，币种信息异常')

        # ===计算下单量、方向、价格
        quantity = row['实际下单量']
        # 按照最小下单量对合约进行四舍五入，对现货就低不就高处理
        # 注意点：合约有reduceOnly参数可以超过你持有的持仓量，现货不行，只能卖的时候留一点点残渣
        quantity = round(quantity, min_qty[symbol]) if symbol_type == 'swap' else truncate(quantity, min_qty[symbol])
        # 计算下单方向、价格，并增加一定的滑点
        if quantity > 0:
            side = 'BUY'
            price = symbol_last_price[symbol] * (1 + slip_rate)
        elif quantity < 0:
            side = 'SELL'
            price = symbol_last_price[symbol] * (1 - slip_rate)
        else:
            print('下单量为0，不进行下单')
            continue
        # 下单量取绝对值
        quantity = abs(quantity)
        # 通过最小价格精度对下单价格进行四舍五入
        price = round(price, price_precision[symbol])

        # ===判断是否是清仓交易
        reduce_only = True if row['交易模式'] == '清仓' and symbol_type == 'swap' else False

        # ===判断交易金额是否小于最小下单金额（一般是5元），小于的跳过
        if quantity * price < min_notional.get(symbol, order_swap_money_limit if symbol_type == 'swap' else order_spot_money_limit):
            if not reduce_only:  # 清仓状态不跳过
                print(symbol, '交易金额是小于最小下单金额（一般合约是5元，现货是10元），跳过该笔交易')
                print('下单量：', quantity, '价格：', price)
                continue

        # ===构建下单参数
        price = f'{price:.{price_precision[symbol]}f}'  # 根据精度将价格转成str
        quantity = np.format_float_positional(quantity).rstrip('.')  # 解决科学计数法的问题
        order_params = {'symbol': symbol, 'side': side, 'type': 'LIMIT', 'price': price, 'quantity': quantity,
                        'newClientOrderId': str(int(time.time())), 'timeInForce': 'GTC', 'reduceOnly': str(bool(reduce_only)),
                        'timestamp': ''}
        # 如果是合约下单，添加进行下单列表中，放便后续批量下单
        if symbol_type == 'swap':
            order_param_list.append(order_params)
        else:  # 如果是现货下单，则直接下单（现货没有批量下单）
            del order_params['reduceOnly']  # 现货没有这个参数，进行移除
            print('现货下单参数：', order_params)
            try:
                # 下单
                order_res = retry_wrapper(exchange.private_post_order, params=order_params, func_name='现货下单')
                print('现货下单完成，现货下单信息结果：', order_res)
            except Exception as e:
                print('现货下单出错：', e)
                send_wechat_work_msg(f'现货 {symbol} 下单 {float(quantity) * float(price)}U 出错，请查看程序日志', wechat_webhook_url)
                continue
            # 发送下单结果到钉钉
            send_msg_for_order([order_params], [order_res], wechat_webhook_url)

    # 批量下单，每5个订单打包执行
    if symbol_type == 'swap':
        print('每个币种的下单参数：', order_param_list)

        for i in range(0, len(order_param_list), 5):
            order_info = order_param_list[i: i + 5]
            print(order_info)
            try:
                # 批量下单
                order_res = retry_wrapper(exchange.fapiPrivate_post_batchorders,
                                          params={'batchOrders': json.dumps(order_info), 'timestamp': ''},
                                          func_name='批量下单')
                print('批量下单完成，批量下单信息结果：', order_res)
            except Exception as e:
                print(e)
                continue
            # 尝试进行补单
            order_info, order_res = retry_order(exchange, order_info, order_res, price_precision)
            # 发送下单结果到钉钉
            send_msg_for_order(order_info, order_res, wechat_webhook_url)
            # 批量下单币种太多，sleep一会儿
            time.sleep(0.5)

    return order_param_list


def retry_order(exchange, order_info, order_res, price_precision, retry_times=5):
    """
    根据计算交易送返回的下单结果，如果有失败，则进行补单

    :param exchange:            交易所对象
    :param order_info:          正常的批量下单参数
    :param order_res:           交易所返回的批量下单结果
    :param price_precision:     币种最小价格精度
    :param retry_times:         重试次数
    """
    # 判断重试次数是否归零
    if retry_times == 0:  # 重试次数归零，直接返回重试结果
        return order_info, order_res
    else:  # 重试次数没有归零，继续重试一下
        retry_times -= 1

    retry_order_info = []  # 定义补单列表
    symbol_last_price = None  # 定义ticker数据
    # ===遍历回调数据
    for _ in range(len(order_info)):
        # 判断当前批量下单的函数里面有没有失败的信息
        if 'msg' in order_res[_].keys():  # 如果有失败的单子，msg会有数据。注意：bn接口返回的数据，是与请求的order_info顺序是一致的
            # 判断是否需要更新symbol_last_price
            if symbol_last_price is None:  # 如果symbol_last_price为空，需要更新一下数据
                symbol_last_price = fetch_binance_ticker_data(exchange)  # 重新获取一下最新的ticker数据

            _symbol = order_info[_]['symbol']  # 获取symbol
            _side = order_info[_]['side']  # 获取下单方向
            if _side == 'BUY':  # 重新计算价格滑点
                price = symbol_last_price[_symbol] * 1.015
            else:
                price = symbol_last_price[_symbol] * 0.985
            order_info[_]['price'] = str(round(price, price_precision[_symbol]))  # 对价格进行四舍五入

            retry_order_info.append(order_info[_])  # 添加到补单列表中

    # ===判断是否有需要补单的数据
    if retry_order_info:  # 若有需要补单的symbol，则进行补单操作
        try:
            # 批量下单
            order_res = retry_wrapper(exchange.fapiPrivate_post_batchorders,
                                      params={'batchOrders': json.dumps(retry_order_info), 'timestamp': ''},
                                      func_name='重试补单的批量下单')
            print('批量补单单完成，批量下单信息结果：', order_res)
        except Exception as e:
            print('重试补单的批量下单接口出错', e)
        # 递归调用重试补单
        # 两个条件退出递归： 1.重试次数归零 2.补单列表为空
        return retry_order(exchange, retry_order_info, order_res, price_precision, retry_times=retry_times)
    else:  # 若没有补单列表，直接返回结果即可
        return order_info, order_res


def get_twap_symbol_info_list(symbol_info, order_amount):
    """
    对超额订单进行拆分,并进行调整,尽可能每批中子订单、每批订单让多空平衡
    :param symbol_info 原始下单信息
    :param order_amount:单次下单最大金额
    """

    # 对下单资金进行拆单
    symbol_info['拆单金额'] = symbol_info['实际下单资金'].apply(lambda x: [x] if abs(x) < order_amount else [(1 if x > 0 else -1) * order_amount] * int(abs(x)/order_amount) + [x % (order_amount if x > 0 else -order_amount)])
    symbol_info['拆单金额'] = symbol_info['拆单金额'].apply(np.array)  # 将list转成numpy的array
    # 计算拆单金额对应多少的下单量
    symbol_info['实际下单量'] = symbol_info['实际下单量'] / symbol_info['实际下单资金'] * symbol_info['拆单金额']
    symbol_info.reset_index(inplace=True)
    del symbol_info['拆单金额']

    # 将拆单量进行数据进行展开
    symbol_info = symbol_info.explode('实际下单量')

    # 定义拆单list
    twap_symbol_info_list = []
    # 分组
    group = symbol_info.groupby(by='index')
    # 获取分组里面最大的长度
    max_group_len = group['index'].size().max()
    if max_group_len > 0:
        # 批量构建拆单数据
        for i in range(max_group_len):
            twap_symbol_info_list.append(group.nth(i).sort_values('实际下单资金', ascending=False).reset_index(drop=True))

    return twap_symbol_info_list


def transfer_bnb(exchange, dust_spot):
    """
    小额资产兑换成BNB，交易所规定6小时可以调用一次接口
    :param exchange:    交易所对象
    :param dust_spot:   当前账户的一些碎单
    :return:
    """
    # 没有碎单直接跳过
    if dust_spot.empty:
        return

    # ===获取小额资产转换的历史(转换接口6H交易一次)
    res = retry_wrapper(exchange.sapiGetAssetDribblet, params={'timestamp': ''}, func_name='获取小额资产转换的历史', if_exit=False)
    if res is None:
        return
    time_list = [int(_['operateTime']) for _ in res['userAssetDribblets']]
    time_list.sort(reverse=True)  # 从大到小排序

    # ===判断执行兑换BNB操作
    if_transfer_bnb = False
    if time_list:
        last_operate_time = datetime.fromtimestamp(time_list[0] / 1000)  # 将时间戳转成日期
        # 当前时间超过最后一次换BNB操作时间15天之后，才可以进行再次换BNB
        if datetime.now() > last_operate_time + timedelta(days=15):
            if_transfer_bnb = True
    else:  # 没有操作记录，表示近期没有兑换过，可以直接兑换
        if_transfer_bnb = True

    # ===执行兑换BNB
    if if_transfer_bnb:
        # 查询可以小额资产换BNB的列表
        res = retry_wrapper(exchange.sapiPostAssetDustBtc, params={'timestamp': ''}, func_name='获取可以转换成BNB的小额资产', if_exit=False)
        if res is None:
            return
        asset_list = [_['asset'] for _ in res['details']]  # 获取可以转换的列表（这个里面会包含你当前的其他持仓）
        # 获取持仓中的碎单
        dust_spot.reset_index(inplace=True)
        dust_list = [_.replace('USDT', '') for _ in dust_spot['symbol'].to_list()]  # 筛选处当前账户的碎单
        # 求出实际需要处理的币种
        common_list = set(asset_list).intersection(set(dust_list))  # 求出 asset_list 与 dust_list 的交集
        common_list = list(common_list)  # 数据转成list
        # 判断是否有币种需要进行兑换BNB
        if common_list:
            # 小额资产换BNB
            print('小额资产换BNB参数:', common_list)
            dust = retry_wrapper(exchange.sapiPostAssetDust, params={'asset': common_list, 'timestamp': ''}, func_name='小额资产转换BNB', if_exit=False)
            print('小额资产换BNB完成，结果返回：', dust)


def cancel_spot_all_orders(exchange):
    # 现货撤单
    orders = retry_wrapper(exchange.private_get_openorders, params={'timestamp': ''}, func_name='查询现货所有挂单')
    symbols = [_['symbol'] for _ in orders]
    symbols = list(set(symbols))
    for _ in symbols:
        retry_wrapper(exchange.private_delete_openorders, params={'symbol': _, 'timestamp': ''}, func_name='取消现货挂单')


def cancel_swap_all_orders(exchange):
    # 合约撤单
    orders = retry_wrapper(exchange.fapiPrivate_get_openorders, params={'timestamp': ''}, func_name='查询合约所有挂单')
    symbols = [_['symbol'] for _ in orders]
    symbols = list(set(symbols))
    for _ in symbols:
        retry_wrapper(exchange.fapiPrivate_delete_allopenorders, params={'symbol': _, 'timestamp': ''}, func_name='取消合约挂单')
